// Copyright (c) 2021 Presto Labs Pte. Ltd.
// Author: daniel

#include "coin2/exchange/gopax/order_v1/parser.h"

#include <utility>

#include "coin/proto/coin_query.pb.h"
#include "coin2/exchange/base/order/order.h"
#include "coin2/exchange/gopax/api_util/gopax_error.h"
#include "coin2/exchange/util/json_util.h"

namespace coin2::exchange::gopax::order_v1 {

namespace {
using coin::proto::CurrencyTransferBundle;
using coin::proto::CurrencyTransferElement;
using coin::proto::TransferRequestProto;

using TransferType = coin::proto::TransferRequestProto::TransferType;
}  // namespace

bool GopaxParser::IsGopaxErrMessage(const JsonValue& data) const {
  // TODO(daniel):
  return data.HasMember("errorCode");
}

auto GopaxParser::GetOrderSide(std::string_view side) const -> OrderSide {
  if (side == "buy") {
    return coin::proto::OrderSide::BUY_ORDER;
  } else if (side == "sell") {
    return coin::proto::OrderSide::SELL_ORDER;
  } else {
    return coin::proto::OrderSide::UNKNOWN_ORDER_SIDE;
  }
}

auto GopaxParser::GetOrderSideFromWs(std::string_view side) const -> OrderSide {
  if (side == "1") {
    return coin::proto::OrderSide::BUY_ORDER;
  } else if (side == "2") {
    return coin::proto::OrderSide::SELL_ORDER;
  } else {
    return coin::proto::OrderSide::UNKNOWN_ORDER_SIDE;
  }
}

auto GopaxParser::GetOrderType(std::string_view type) const -> OrderType {
  if (type == "limit") {
    return coin::proto::OrderType::LIMIT_ORDER;
  } else if (type == "market") {
    return coin::proto::OrderType::MARKET_ORDER;
  } else {
    return coin::proto::OrderType::UNKNOWN_ORDER_TYPE;
  }
}

auto GopaxParser::GetOrderTypeFromWs(std::string_view type) const -> OrderType {
  if (type == "1") {
    return coin::proto::OrderType::LIMIT_ORDER;
  } else if (type == "2") {
    return coin::proto::OrderType::MARKET_ORDER;
  } else {
    return coin::proto::OrderType::UNKNOWN_ORDER_TYPE;
  }
}

void GopaxParser::SetOrderStatus(const JsonValue& data, ProductOrderElement* product_order_element)
    const {
  std::string_view status = data["status"].GetString();
  if (status == "placed" || status == "updated" || status == "reserved") {
    product_order_element->set_is_live(true);
  } else if (status == "completed") {
    product_order_element->set_is_live(false);
  } else if (status == "cancelled") {
    product_order_element->set_cancel_confirmed(true);
  } else {
    LOG(ERROR) << "[ParseRestOrderInfo] Unknown order status! " << status;
  }
}

void GopaxParser::SetOrderStatusFromWs(
    const JsonValue& data,
    ProductOrderElement* product_order_element) const {
  // NOTE: status = 3 may due to post only
  // status: 1(not filled), 2(canceled), 3(completely filled), 4(partially filled), 5(reserved)
  std::string_view status = data["status"].GetString();
  if (status == "1" || status == "4" || status == "5") {
    product_order_element->set_is_live(true);
  } else if (status == "3") {
    if (data.HasMember("forcedCompletionReason") &&
        0 != std::stoi(data["forcedCompletionReason"].GetString())) {
      product_order_element->set_cancel_confirmed(true);
    } else {
      product_order_element->set_fully_filled(true);
    }
  } else if (status == "2") {
    product_order_element->set_cancel_confirmed(true);
  } else {
    LOG(ERROR) << "[ParseRestOrderInfo] Unknown order status! exchange_order_id="
               << product_order_element->exchange_order_id() << ", status=" << status;
  }
}

void GopaxParser::ParseSubmitResponse() {
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::SUBMIT_RESPONSE);
  auto resp = result_.mutable_submit_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[GopaxSubmitOrder] AsioHttpContextStatus Error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }

  if (http_context->res.result_int() == 429) {
    LOG(ERROR) << "[GopaxSubmitOrder] failed [" << order_context->proc_order_id() << "], "
               << "API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[GopaxSubmitOrder] Parsing Error." << msg_->DebugString();
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }
  if (IsGopaxErrMessage(document)) {
    LOG(ERROR) << "[GopaxSubmitOrder] failed. id: [" << order_context->proc_order_id() << "], "
               << http_context->res;
    const std::string error_code = document["errorCode"].GetString();
    const std::string error_msg = api_util::GopaxError::GetGopaxErrorMsg(error_code);
    resp->set_exchange_error_code(error_code);
    resp->set_exchange_error_msg(error_msg);
    // TODO(daniel): group error
    if ("201" == error_code) {
      resp->set_error_code(coin::proto::OrderErrorCode::SUBMIT_ORDER_INSUFFICIENT_FUND);
    } else {
      resp->set_error_code(coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR);
    }
    resp->set_success(false);
    return;
  }
  resp->set_exchange_order_id(document["id"].GetString());
  resp->set_success(true);

  // handle forced completed order due to timeInForce or protection
  const std::string status = document["status"].GetString();
  if ("completed" == status && document.HasMember("forcedCompletionReason")) {
    // TODO(daniel): to confirm what's the status and forcedCompletionReason when an order
    // filled
    auto pob = result_.mutable_order_list();
    pob->Clear();
    auto product_order_element = pob->add_each_order();
    product_order_element->set_market_type(MarketType::Spot);
    product_order_element->set_exchange_type(ExchangeType::Gopax);
    product_order_element->set_api_version("v1");
    product_order_element->set_exchange_order_id(document["id"].GetString());
    product_order_element->set_symbol(document["tradingPairName"].GetString());
    product_order_element->set_price(stod(document["price"].GetString()));
    product_order_element->set_qty(stod(document["amount"].GetString()));
    product_order_element->set_proc_order_id(stoll(document["clientOrderId"].GetString()));
    product_order_element->set_side(GetOrderSide(document["side"].GetString()));
    product_order_element->set_order_type(GetOrderType(document["type"].GetString()));
    product_order_element->set_timestamp(msg_->timestamp);
    product_order_element->set_cancel_confirmed(true);
    result_.set_type(MsgType::ORDER_STATUS);
  }
}

void GopaxParser::ParseCancelResponse() {
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::CANCEL_RESPONSE);
  auto resp = result_.mutable_cancel_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_exchange_order_id(order_context->exchange_order_id());
  resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    LOG(ERROR) << "[GopaxCancelOrder] AsioHttpContextStatus Error";
    return;
  }
  if (http_context->res.result_int() == 429) {
    LOG(ERROR) << "[GopaxCancelOrder] failed [" << order_context->proc_order_id() << "], "
               << "API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[GopaxCancelOrder] Parsing Error." << msg_->DebugString();
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }
  if (IsGopaxErrMessage(document)) {
    LOG(ERROR) << "[GopaxCancelOrder] failed. id: [" << order_context->proc_order_id() << "], "
               << http_context->res;
    std::string error_code = document["errorCode"].GetString();
    std::string error_msg = api_util::GopaxError::GetGopaxErrorMsg(error_code);
    resp->set_exchange_error_code(error_code);
    resp->set_exchange_error_msg(error_msg);
    // TODO(daniel): group error
    if ("10069" == error_code || "10221" == error_code) {
      resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NOT_FOUND);
    } else {
      resp->set_error_code(coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR);
    }
    resp->set_success(false);
    return;
  }
  resp->set_success(true);
  resp->set_cancel_confirmed(true);
}

void GopaxParser::ParseRestOrderInfo(
    const JsonValue& data,
    ProductOrderElement* product_order_element) {
  /*
  {
    "id": "453324",                           # order ID
    "clientOrderId": "zeckrw23456",           # client order ID (showed only when it exists)
    "status": "updated",                      # placed, cancelled, completed, updated, reserved
    "tradingPairName": "ZEC-KRW",             # order book
    "side": "buy",                            # buy, sell
    "type": "limit",                          # limit, market
    "price": 1000000,                         # price
    "stopPrice": undefined,                   # stop price (showed only for stop orders)
    "amount": 4,                              # initial amount
    "remaining": 1,                           # outstanding amount
    "protection": "yes",                      # whether protection is activated (yes or no)
    "timeInForce": "gtc",                     # limit order's time in force (gtc/po/ioc/fok)
    "createdAt": "2020-09-25T04:06:20.000Z",  # order placement time
    "updatedAt": "2020-09-25T04:06:29.000Z",  # order last update time
    "balanceChange": {                        #
      "baseGross": 3,                         # base asset balance's gross change
      "baseFee": {                            #
        "taking": 0,                          # base asset fee imposed as taker
        "making": -0.0012                     # base asset fee imposed as maker
      },                                      #
      "baseNet": 2.9988,                      # base asset balance's net change
      "quoteGross": -3000000,                 # quote asset balance's gross change (in KRW for)
      "quoteFee": {                           #
        "taking": 0,                          # quote asset fee imposed as taker
        "making": 0                           # quote asset fee imposed as maker
      },                                      #
      "quoteNet": -3000000                    # quote asset balance's net change
    }
  }
  */

  product_order_element->set_market_type(MarketType::Spot);
  product_order_element->set_exchange_type(ExchangeType::Gopax);
  product_order_element->set_api_version("v1");
  product_order_element->set_exchange_order_id(data["id"].GetString());
  product_order_element->set_symbol(data["tradingPairName"].GetString());
  product_order_element->set_price(stod(data["price"].GetString()));
  product_order_element->set_qty(stod(data["amount"].GetString()));
  product_order_element->set_side(GetOrderSide(data["side"].GetString()));
  product_order_element->set_order_type(GetOrderType(data["type"].GetString()));
  product_order_element->set_timestamp(msg_->timestamp);
  SetOrderStatus(data, product_order_element);
  // somehow there no field of "clientOrderId"
  if (data.HasMember("clientOrderId")) {
    product_order_element->set_proc_order_id(stoll(data["clientOrderId"].GetString()));
  }
}

auto GopaxParser::ParseRestOrderInfoFromWs(const JsonValue& data) -> ProductOrderBundle {
  /*
  [
    {
      "orderId": 327347,            # order ID
      "status": 1,                  # 1(not filled), 2(canceled), 3(completely filled) ...
      "side": 2,                    # 1(bid), 2(ask)
      "type": 1,                    # 1(limit), 2(market)
      "price": 5500000,             # price
      "orgAmount": 1,               # initially placed amount
      "remainAmount": 1,            # unfilled or remaining amount
      "createdAt": 1597218137,      # placement time
      "updatedAt": 1597218137,      # last update time
      "tradedBaseAmount": 0,        # filled base asset amount (in ZEC for this case)
      "tradedQuoteAmount": 0,       # filled quote asset amount (in KRW for this case)
      "feeAmount": 0,               # fee amount
      "rewardAmount": 0,            # reward amount
      "timeInForce": 0,             # 0(gtc), 1(post only), 2(ioc), 3(fok)
      "protection": 1,              # 1(not applied), 2(applied)
      "forcedCompletionReason": 0,  # 0(n/a), 1(timeInForce), 2(protection)
      "stopPrice": 0,               # stop price (> 0 only for stop orders)
      "takerFeeAmount": 0,          # fee amount paid as a taker position
      "tradingPairName": "ZEC-KRW"  # order book
    },
    ...
  ]
  */
  // NOTE: seems only open order will be returned here
  ProductOrderBundle product_order_bundle;
  if (!data.IsArray()) {
    LOG(ERROR) << "[ParseRestOrderInfoFromWs] fail. " << JsonToString(data);
    return product_order_bundle;
  }
  const auto& symbols = native_symbols();
  for (const auto& info : data.GetArray()) {
    const auto& symbol = info["tradingPairName"].GetString();
    if (symbols.count(symbol) == 0) {
      continue;
    }
    auto product_order_element = product_order_bundle.add_each_order();
    product_order_element->set_market_type(MarketType::Spot);
    product_order_element->set_exchange_type(ExchangeType::Gopax);
    product_order_element->set_api_version("v1");
    product_order_element->set_exchange_order_id(info["orderId"].GetString());
    product_order_element->set_symbol(symbol);
    product_order_element->set_price(stod(info["price"].GetString()));
    product_order_element->set_qty(stod(info["orgAmount"].GetString()));
    product_order_element->set_side(GetOrderSideFromWs(info["side"].GetString()));
    product_order_element->set_order_type(GetOrderTypeFromWs(info["type"].GetString()));
    product_order_element->set_timestamp(msg_->timestamp);
    SetOrderStatusFromWs(info, product_order_element);
  }

  return product_order_bundle;
}

auto GopaxParser::ParseRestOrderInfoFromWsDelta(const JsonValue& data) -> ProductOrderBundle {
  /*
  {
    "i": 550862,
    "n": "OrderEvent",
    "o": {
      "orderId": 1739024739,
      "status": 3,
      "side": 2,
      "type": 1,
      "price": 4771,
      "orgAmount": 3,
      "remainAmount": 3,
      "createdAt": 1624264926,
      "updatedAt": 1624264926,
      "tradedBaseAmount": 0,
      "tradedQuoteAmount": 0,
      "feeAmount": 0,
      "rewardAmount": 0,
      "timeInForce": 1,
      "protection": 0,
      "forcedCompletionReason": 1,
      "stopPrice": 0,
      "takerFeeAmount": 0,
      "tradingPairName": "EOS-KRW"
    }
  },
  {
    "i": "-1",
    "n": "OrderEvent",
    "o": {
      "orderId": "1739029601",
      "status": "3",
      "side": "2",
      "type": "1",
      "price": "4761",
      "orgAmount": "3",
      "remainAmount": "0",
      "createdAt": "1624264989",
      "updatedAt": "1624264992",
      "tradedBaseAmount": "3",
      "tradedQuoteAmount": "14283",
      "feeAmount": "0",
      "rewardAmount": "0",
      "timeInForce": "1",
      "protection": "0",
      "forcedCompletionReason": "0",
      "stopPrice": "0",
      "takerFeeAmount": "0",
      "tradingPairName": "EOS-KRW"
    }
  }
  */
  ProductOrderBundle product_order_bundle;
  if (!data.HasMember("tradingPairName")) {
    return product_order_bundle;
  }
  const auto& symbols = native_symbols();
  const auto& symbol = data["tradingPairName"].GetString();
  if (symbols.count(symbol) == 0) {
    return product_order_bundle;
  }
  auto product_order_element = product_order_bundle.add_each_order();
  product_order_element->set_market_type(MarketType::Spot);
  product_order_element->set_exchange_type(ExchangeType::Gopax);
  product_order_element->set_api_version("v1");
  product_order_element->set_exchange_order_id(data["orderId"].GetString());
  product_order_element->set_symbol(symbol);
  product_order_element->set_price(stod(data["price"].GetString()));
  product_order_element->set_qty(stod(data["orgAmount"].GetString()));
  product_order_element->set_side(GetOrderSideFromWs(data["side"].GetString()));
  product_order_element->set_order_type(GetOrderTypeFromWs(data["type"].GetString()));
  product_order_element->set_timestamp(msg_->timestamp);
  SetOrderStatusFromWs(data, product_order_element);

  return product_order_bundle;
}

void GopaxParser::ParseRestFillInfoList() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestFillInfoList] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  if (document.IsObject() && IsGopaxErrMessage(document)) {
    LOG(ERROR) << "[ParseRestFillInfoList] failed. " << JsonToString(document);
    return;
  }
  if (!document.IsArray()) {
    LOG(ERROR) << "[ParseRestFillInfoList] failed. " << JsonToString(document);
    return;
  }

  auto pfb = result_.mutable_fill_list();
  pfb->Clear();

  if (ParseRestFillInfo(document, pfb)) {
    result_.set_type(MsgType::ORDER_FILL);
  }
}

auto GopaxParser::ParseRestFillInfoListFromWsDelta(const JsonValue& data) -> ProductFillBundle {
  /*
  {
    "tradeId": 74072,             # trade ID
    "orderId": 453529,            # order ID
    "side": 2,                    # 1(bid), 2(ask)
    "type": 1,                    # 1(limit), 2(market)
    "baseAmount": 0.01,           # filled base asset amount (in ZEC for this case)
    "quoteAmount": 1,             # filled quote asset amount (in KRW for this case)
    "fee": 0.0004,                # fee
    "price": 100,                 # price
    "isSelfTrade": false,         # whether both of matching orders are yours
    "occurredAt": 1603932107,     # trade occurrence time
    "tradingPairName": "ZEC-KRW"  # order book
  }
  */
  ProductFillBundle product_fill_bundle;
  auto fill = product_fill_bundle.add_each_fill();
  fill->set_symbol(data["tradingPairName"].GetString());
  fill->set_fill_timestamp(msg_->timestamp);
  fill->set_exchange_order_id(data["orderId"].GetString());
  fill->set_price(stod(data["price"].GetString()));
  fill->set_qty(stod(data["baseAmount"].GetString()));
  fill->set_fill_id(data["tradeId"].GetString());
  fill->set_fee(stod(data["fee"].GetString()));

  return product_fill_bundle;
}

bool GopaxParser::ParseRestFillInfo(const JsonValue& data, ProductFillBundle* product_fill_bundle) {
  /*
  [
    {
      "id": 73953,                              # trading event ID
      "orderId": 453324,                        # order ID
      "baseAmount": 3,                          # traded base asset amount
      "quoteAmount": 3000000,                   # traded quote asset amount
      "fee": 0.0012,                            # fee
      "price": 1000000,                         # price
      "timestamp": "2020-09-25T04:06:30.000Z",  # trading time
      "side": "buy",                            # buy, sell
      "tradingPairName": "ZEC-KRW",             # order book
      "position": "maker"                       # maker, taker
    },
    ...
  ]
  */
  const auto& symbols = native_symbols();
  bool product_fill_bundle_found = false;
  for (const auto& info : data.GetArray()) {
    const auto& symbol = info["tradingPairName"].GetString();
    if (symbols.count(symbol) == 0) {
      continue;
    }
    auto fill = product_fill_bundle->add_each_fill();
    fill->set_symbol(symbol);
    fill->set_fill_timestamp(msg_->timestamp);
    fill->set_exchange_order_id(info["orderId"].GetString());
    fill->set_price(stod(info["price"].GetString()));
    fill->set_qty(stod(info["baseAmount"].GetString()));
    fill->set_fill_id(info["id"].GetString());
    fill->set_fee(stod(info["fee"].GetString()));
    const std::string fill_type = info["position"].GetString();
    if (fill_type == "maker") {
      fill->set_fill_type(coin::proto::FillType::MAKER_FILL_TYPE);
    } else if (fill_type == "taker") {
      fill->set_fill_type(coin::proto::FillType::TAKER_FILL_TYPE);
    }
    product_fill_bundle_found = true;
  }
  return product_fill_bundle_found;
}

void GopaxParser::ParseRestAllOrders() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestOpenOrders] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  if (document.IsObject() && IsGopaxErrMessage(document)) {
    LOG(ERROR) << "[ParseRestOpenOrders] failed. " << JsonToString(document);
    return;
  }
  if (!document.IsArray()) {
    LOG(ERROR) << "[ParseRestOpenOrders] failed. " << JsonToString(document);
    return;
  }

  auto pob = result_.mutable_order_list();
  pob->Clear();
  bool product_order_bundle_found = false;
  const auto& symbols = native_symbols();
  for (const auto& info : document.GetArray()) {
    const auto& symbol = info["tradingPairName"].GetString();
    if (symbols.count(symbol) == 0) {
      continue;
    }
    const std::string order_status = info["status"].GetString();
    if (order_status == "completed") {
      // ignore process fully filled/forced completed(PO/FOK) order
      // due to cann't know if an order is fully filled or force completed
      continue;
    }
    auto order = pob->add_each_order();
    ParseRestOrderInfo(info, order);
    product_order_bundle_found = true;
  }
  if (product_order_bundle_found) {
    result_.set_type(MsgType::ORDER_STATUS);
  }
}

// NOTE: when field exchange and market_type are set, which means parsed successfully
auto GopaxParser::ParseBalance(const JsonValue& doc) -> AccountBalance {
  RawBalanceMap raw_balance_map;
  auto success = ParseRawBalance(doc, &raw_balance_map);
  if (!success) {
    return {};
  }

  AccountBalance acc_balance;
  acc_balance.set_exchange("Gopax");
  acc_balance.set_market_type("Spot");
  for (const auto& native_currency : symcache_->GetCurrenciesNativeUpper()) {
    auto* balance = acc_balance.add_each_balance();
    if (raw_balance_map.count(native_currency) == 0) {
      balance->set_total(0);
      balance->set_hold(0);
      balance->set_available(0);
    } else {
      balance->MergeFrom(raw_balance_map.at(native_currency));
    }
    auto* currency = symcache_->GetCurrencyByNative(native_currency);
    balance->set_currency(currency->currency());
    balance->set_currency_native(currency->native_currency());
    DLOG(INFO) << "BALANCE: " << currency->native_currency() << " " << balance->total() << " "
               << balance->available();
  }
  is_account_balance_initialized_ = true;

  return acc_balance;
}

bool GopaxParser::ParseRawBalance(const JsonValue& doc, RawBalanceMap* raw_balance_map) {
  /*
  [
    {
      "asset": "KRW",                   # asset name
      "avail": 1759466.76,              # available amount to place order
      "hold": 16500,                    # outstanding amount on order books
      "pendingWithdrawal": 0,           # amount being withdrawan
      "lastUpdatedAt": "1600684352032"  # balance last update time
    },
    {
      "asset": "ETH",
      "avail": 100979.57523727,
      "hold": 50.02,
      "pendingWithdrawal": 0,
      "lastUpdatedAt": "1600686162591"
    },
    ...
  ]
  */

  if (doc.IsObject() && IsGopaxErrMessage(doc)) {
    LOG(ERROR) << "[ParseBalance] failed. " << JsonToString(doc);
    return false;
  }
  if (!doc.IsArray()) {
    LOG(ERROR) << "[ParseBalance] failed. " << JsonToString(doc);
    return false;
  }

  for (auto& item : doc.GetArray()) {
    if (!item.HasMember("asset")) {
      continue;
    }
    CurrencyBalance currency_balance;
    currency_balance.set_currency_native(item["asset"].GetString());
    currency_balance.set_available(stod(item["avail"].GetString()));
    currency_balance.set_hold(stod(item["hold"].GetString()));
    currency_balance.set_total(currency_balance.available() + currency_balance.hold());
    raw_balance_map->emplace(currency_balance.currency_native(), currency_balance);
  }

  return true;
}

auto GopaxParser::ParseBalanceFromWs(const JsonValue& data) -> AccountBalance {
  /*
  [
    {
      "assetId": 1,
      "avail": 48884.92713185,
      "hold": 0,
      "pendingWithdrawal": 0,
      "blendedPrice": 1,
      "lastUpdatedAt": 1624016755.13,
      "isoAlpha3": "KRW"
    },
    {
      "assetId": 3,
      "avail": 0,
      "hold": 0,
      "pendingWithdrawal": 0,
      "blendedPrice": 0,
      "lastUpdatedAt": 0,
      "isoAlpha3": "ETH"
    },
    ...
  ]
  */
  AccountBalance acc_balance;
  if (!data.IsArray()) {
    LOG(ERROR) << "[ParseBalanceFromWs] fail. " << JsonToString(data);
    return acc_balance;
  }
  acc_balance.set_exchange("Gopax");
  acc_balance.set_market_type("Spot");
  for (auto& item : data.GetArray()) {
    if (!item.HasMember("isoAlpha3")) {
      continue;
    }
    const auto& native_currency = item["isoAlpha3"].GetString();
    if (!symcache_->has_currency_native(native_currency)) {
      DLOG(INFO) << "[ParseBalanceFromWs] ignored unknown currency: " << native_currency;
      continue;
    }
    auto* currency = symcache_->GetCurrencyByNative(native_currency);
    auto* currency_balance = acc_balance.add_each_balance();
    currency_balance->set_currency(currency->currency());
    currency_balance->set_currency_native(currency->native_currency());
    currency_balance->set_available(stod(item["avail"].GetString()));
    currency_balance->set_hold(stod(item["hold"].GetString()));
    currency_balance->set_total(currency_balance->available() + currency_balance->hold());
  }
  is_account_balance_initialized_ = true;

  return acc_balance;
}

auto GopaxParser::ParseBalanceFromWsDelta(const JsonValue& data) -> AccountBalance {
  /*
  {
    "assetId": 1,
    "avail": 48884.92713185,
    "hold": 0,
    "pendingWithdrawal": 0,
    "blendedPrice": 1,
    "lastUpdatedAt": 1624016755.13,
    "isoAlpha3": "KRW"
  }
  */
  AccountBalance acc_balance;
  if (!is_account_balance_initialized_) {
    return acc_balance;
  }
  if (!data.HasMember("isoAlpha3")) {
    return acc_balance;
  }
  const auto& native_currency = data["isoAlpha3"].GetString();
  if (!symcache_->has_currency_native(native_currency)) {
    DLOG(INFO) << "[ParseBalanceFromWs] ignored unknown currency: " << native_currency;
    return acc_balance;
  }
  auto* currency = symcache_->GetCurrencyByNative(native_currency);
  acc_balance.set_exchange("Gopax");
  acc_balance.set_market_type("Spot");
  auto* currency_balance = acc_balance.add_each_balance();
  currency_balance->set_currency(currency->currency());
  currency_balance->set_currency_native(currency->native_currency());
  currency_balance->set_available(stod(data["avail"].GetString()));
  currency_balance->set_hold(stod(data["hold"].GetString()));
  currency_balance->set_total(currency_balance->available() + currency_balance->hold());

  return acc_balance;
}

void GopaxParser::ParseRestAccountBalance() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestAccountBalance] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  auto account_balance = ParseBalance(document);
  if (!account_balance.has_exchange() || !account_balance.has_market_type()) {
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  result_.set_type(MsgType::ACCOUNT_BALANCE);
  *result_.mutable_account_balance() = std::move(account_balance);
}

void GopaxParser::ParseRestTransfer() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestTransfer] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  CurrencyTransferBundle* currency_transfer_bundle = ParseTransfer(document);
  if (currency_transfer_bundle == nullptr || !currency_transfer_bundle->has_exchange() ||
      !currency_transfer_bundle->has_market_type()) {
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  result_.set_type(MsgType::TRANSFER);
}

// NOTE: when field exchange and market_type are set, which means parsed successfully
CurrencyTransferBundle* GopaxParser::ParseTransfer(const JsonValue& data) {
  /*
  [
    {
      "id": 640,                      # deposit/withdrawal event ID
      "asset": "BTC",                 # asset name
      "type": "crypto_withdrawal",    # fiat_withdrawal, fiat_deposit ...
      "netAmount": 0.0001,            # amount
      "feeAmount": 0.0005,            # fee (null if there is no imposed fee)
      "status": "completed",          # reviewing, rejected, processing, failed, completed
      "reviewStartedAt": 1595556218,  # request time
      "completedAt": 1595556902,      # completion time (showed only in case of completed)
      "txId": "eaca5ad3...",          # tx ID
      "sourceAddress": null,          # sender address (showed only in case of crypto_deposit)
      "destinationAddress: "3H8...",  # recipient address (showed only in case of crypto_withdrawal)
      "destinationMemoId": null       # recipient address's memo ID
    }
  ]
  */
  if (data.IsObject() && IsGopaxErrMessage(data)) {
    LOG(ERROR) << "[ParseTransfer] Error message" << JsonToString(data);
    return nullptr;
  }

  if (!data.IsArray()) {
    LOG(ERROR) << "[ParseTransfer] Invalid data format" << JsonToString(data);
    return nullptr;
  }

  auto* currency_transfer_bundle = result_.mutable_currency_transfer_bundle();
  currency_transfer_bundle->Clear();
  currency_transfer_bundle->set_exchange("Gopax");
  currency_transfer_bundle->set_market_type("Spot");
  for (const auto& item : data.GetArray()) {
    if (!item.HasMember("asset") || !item.HasMember("type") || !item.HasMember("netAmount")) {
      LOG(ERROR) << "[ParseTransfer] Invalid transfer format";
      continue;
    }
    const auto& native_currency = item["asset"].GetString();
    if (!symcache_->has_currency_native(native_currency)) {
      DLOG(INFO) << "[ParseTransfer] ignored unknown currency: " << native_currency;
      continue;
    }
    auto* currency = symcache_->GetCurrencyByNative(native_currency);
    auto* currency_transfer = currency_transfer_bundle->add_each_transfer();
    currency_transfer->set_currency(currency->currency());
    std::string type = item["type"].GetString();
    if (type == "fiat_withdrawal") {
      currency_transfer->set_type(TransferRequestProto::WITHDRAW);
    } else if (type == "fiat_deposit") {
      currency_transfer->set_type(TransferRequestProto::DEPOSIT);
    } else if (type == "crypto_withdrawal") {
      currency_transfer->set_type(TransferRequestProto::WITHDRAW);
    } else if (type == "crypto_deposit") {
      currency_transfer->set_type(TransferRequestProto::DEPOSIT);
    }
    std::string status = item["status"].GetString();
    if (status == "completed") {
      currency_transfer->set_status(TransferRequestProto::TRANSFER_CONFIRMED);
    }
    currency_transfer->set_transfer_id(item["txId"].GetString());
    currency_transfer->set_qty(stod(item["netAmount"].GetString()));
    if (item.HasMember("feeAmount") && !item["feeAmount"].IsNull()) {
      currency_transfer->set_fee(stod(item["feeAmount"].GetString()));
    }
    currency_transfer->set_create_timestamp(stoll(item["reviewStartedAt"].GetString()) * 1e9);
  }
  return currency_transfer_bundle;
}

void GopaxParser::ParseWsMessage() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseWsMessage] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  DLOG(INFO) << "OG ws msg: " << JsonToString(document);
  if (!document.HasMember("n") || !document.HasMember("o")) {
    LOG(ERROR) << "[ParseWsMessage] invalid msg. " << msg_->DebugString();
    return;
  }
  auto obj = document["o"].GetObject();
  bool result = true;
  if (obj.HasMember("result")) {
    result = obj["result"].GetBool();
  }
  if (!result) {
    LOG(ERROR) << "[ParseWsMessage] failed. " << msg_->DebugString();
    return;
  }
  std::string_view type = document["n"].GetString();
  if (type == "SubscribeToBalances") {
    // response of subcribe balance
    auto account_balance = ParseBalanceFromWs(obj["data"]);
    result_.set_type(MsgType::ACCOUNT_BALANCE);
    if (!account_balance.has_exchange() || !account_balance.has_market_type()) {
      result_.set_error_code(OrderErrorCode::INVALID_JSON);
      return;
    }
    result_.set_type(MsgType::ACCOUNT_BALANCE);
    *result_.mutable_account_balance() = std::move(account_balance);
  } else if (type == "SubscribeToOrders") {
    // response of subcribe orders
    *result_.mutable_order_list() = ParseRestOrderInfoFromWs(obj["data"]);
    result_.set_type(MsgType::ORDER_STATUS);
  } else if (type == "SubscribeToTrades") {
    // response of subcribe trades. an empty response is returned
  } else if (type == "BalanceEvent") {
    // balance delta push
    auto account_balance = ParseBalanceFromWsDelta(obj);
    if (!account_balance.has_exchange() || !account_balance.has_market_type()) {
      result_.set_error_code(OrderErrorCode::INVALID_JSON);
      return;
    }
    result_.set_type(MsgType::ACCOUNT_BALANCE);
    *result_.mutable_account_balance() = std::move(account_balance);
  } else if (type == "OrderEvent") {
    // order delta push
    *result_.mutable_order_list() = ParseRestOrderInfoFromWsDelta(obj);
    result_.set_type(MsgType::ORDER_STATUS);
  } else if (type == "TradeEvent") {
    // trade delta push
    *result_.mutable_fill_list() = ParseRestFillInfoListFromWsDelta(obj);
    result_.set_type(MsgType::ORDER_FILL);
  } else {
    LOG(ERROR) << "[ParseWsMessage] unknown ws message. " << msg_->DebugString();
  }
}

auto GopaxParser::ParseRawMessage(const RawMessage& msg) -> const ParseResult& {
  result_.Clear();
  msg_ = &msg;
  sub_topic_id_ = static_cast<OrderSubTopicId>(msg.type);

  if (msg.type == OrderSubTopicId::WS_MESSAGE) {
    try {
      ParseWsMessage();
    } catch (...) {
      LOG(ERROR) << "exception catched here, payload: " << msg_->data->data();
      std::rethrow_exception(std::current_exception());
    }
    return result_;
  }

  switch (msg.http_context->query_type) {
    case OrderSubTopicId::SUBMIT_RESPONSE:
      ParseSubmitResponse();
      break;
    case OrderSubTopicId::CANCEL_RESPONSE:
      ParseCancelResponse();
      break;
    case OrderSubTopicId::MULTI_CANCEL_RESPONSE:
      // pass
      break;
    case OrderSubTopicId::REST_ALL_ORDERS:
      ParseRestAllOrders();
      break;
    case OrderSubTopicId::REST_FILL_ORDERS:
      ParseRestFillInfoList();
      break;
    case OrderSubTopicId::REST_ACCOUNT_BALANCE:
      ParseRestAccountBalance();
      break;
    case OrderSubTopicId::TRANSFER:
      ParseRestTransfer();
      break;
    default:
      LOG(ERROR) << "Parsing Error. " << msg_->DebugString();
      break;
  }

  return result_;
}

}  // namespace coin2::exchange::gopax::order_v1
